<script setup lang="ts">
import { Terminal, FailedFunc, SuccessFunc, TerminalApi } from 'vue-web-terminal'
import stripAnsi from 'strip-ansi'
import splitLines from 'split-lines'
import { TerminalApiPushMsg } from '@xtail/vue3-web-terminal'

const {
  name = 'terminal',
  info = '',
  warning = '',
  show = true,
  cmdDoneHandler = async () => {}
} = defineProps<{
  // 终端名称
  name?: string
  // 终端挂载后显示的提示信息
  info?: string
  // 终端挂载后显示的警告信息
  warning?: string
  // 是否显示终端及操作区域按钮等
  show?: boolean
  // 命令执行完成回调函数
  cmdDoneHandler?: () => Promise<void>
}>()

const terminalApiPushMsg = new TerminalApiPushMsg(TerminalApi)

const cmdExecDataPre = ref<string>('')
const cmdDone = ref<boolean>(false)
let cmdDoneCnt = 0
const cmdPrompt = ref<string>('>')

const handleCmdDone = async () => {
  await cmdDoneHandler()
}

onMounted(async () => {
  terminalApiPushMsg.info(name, '如果未选择连接服务器，则默认与本机终端建立会话连接')
  if (info) terminalApiPushMsg.info(name, info)
  if (warning) terminalApiPushMsg.warning(name, warning)
  // 监听进程通信，接收命令的执行结果
  // 参数 1：进程通信事件对象
  // 参数 2：数据
  // 参数 3：会话是否关闭
  addIpcListener(name, async (evt, data: string, close: boolean) => {
    const dataStripAnsiTrim = stripAnsi(data).trim()
    // 内容为空的数据不输出
    if (StrUtil.isEmpty(dataStripAnsiTrim)) return
    // 如果终端会话关闭，则接收到的 data 为提示信息，不再是命令执行结果
    if (close) return await disconnect()
    // 1. 由于返回的数据中可能包含多行，即命令执行结果可能包含在命令执行完成标记返回的数据中，所以需要逐行判断；
    // 2. 在处理命令执行结果中，防止命令提示符（命令执行结果最后一行）和命令执行结果数据一起返回，所以也要分行处理；
    let dataLines = splitLines(data)
    // 去除内容为空的行（存在看不见的特殊字符，stripAnsi 处理后行内容会为空）
    dataLines = dataLines.filter((dataLine) => stripAnsi(dataLine).trim())
    // 如果接收到的数据为命令执行完成标记
    if (isCmdDone(dataStripAnsiTrim)) {
      // 先获取返回的数据中的最后一行，更新命令提示符
      cmdPrompt.value = stripAnsi(ArrUtil.getLast(dataLines)).trim()
      // 由于返回的数据中可能包含多行，即命令执行结果可能包含在命令执行完成标记返回的数据中，所以需要逐行判断；
      for (let i = 0; i < dataLines.length; i++) {
        const line = dataLines[i]
        const lineStripAnsiTrim = stripAnsi(line).trim()
        // 如果是命令执行完成标记，则跳过该行数据
        if (isCmdDone(lineStripAnsiTrim)) continue
        // 由于命令执行完成标记返回数据中可能包含命令提示符，且命令执行结果中也可能包含命令提示符，
        // 所以这里出现与上一次命令执行结果相同，则跳过该行数据
        // 这里的判断为 true 的情况为：之前的返回结果数据为命令提示符，当前返回结果数据也为命令提示符
        if (stripAnsi(cmdExecDataPre.value).includes(cmdPrompt.value)) continue
        // 由于到命令标记执行完成前的返回结果中，有可能包含命令提示符，所以这里需要判断
        // 如果之前的结果数据不为命令提示符，则输出之前的命令执行结果
        if (cmdExecDataPre.value) {
          TerminalApi.pushMessage(name, {
            type: 'ansi',
            content: cmdExecDataPre.value
          })
        }
        cmdExecDataPre.value = line
      }
      cmdDone.value = true
      cmdDoneCnt++
      if (cmdDoneCnt === 1) {
        await handleCmdDone()
      }
    } else {
      // 命令还未执行完成，输出命令执行结果
      if (!cmdDone.value) {
        // 处理命令执行结果，防止命令提示符（命令执行结果最后一行）和命令执行结果数据一起返回，所以也要分行处理
        for (let i = 0; i < dataLines.length; i++) {
          const line = dataLines[i]
          if (cmdExecDataPre.value) {
            TerminalApi.pushMessage(name, {
              type: 'ansi',
              content: cmdExecDataPre.value
            })
          }
          cmdExecDataPre.value = line
        }
      }
      // 命令执行完成，更新命令提示符
      // 由于命令执行完成标记不会进入该分支，所以执行到这里，返回的数据为命令提示符
      else {
        const prompt = dataStripAnsiTrim
        // 由于前面都分行对命令执行结果数据进行处理，所以这里可以直接进行判断
        if (cmdExecDataPre.value && !stripAnsi(cmdExecDataPre.value).includes(prompt)) {
          TerminalApi.pushMessage(name, {
            type: 'ansi',
            content: cmdExecDataPre.value
          })
        }
        cmdPrompt.value = prompt
      }
    }
  })
})

// 选择的服务器主机连接配置
const hostConnConfSelected = ref<HostConnConfType>()
const selectHostConnConf = (conf?: HostConnConfType) => {
  const hostConnConf = conf ? { ...conf } : conf
  if (hostConnConf && hostConnConf.icon) hostConnConf.icon = ''
  hostConnConfSelected.value = hostConnConf
}

const settingIconProps = SIconPropsConst.Settings
const isShowSettings = ref(false)
const settingsForm = ref<{
  // 检测是否与服务器保持连接的时间间隔（单位：秒）
  keepaliveInterval: number
  // 检测是否与服务器保持连接，所允许的最大未应答次数
  keepaliveCountMax: number
}>({
  keepaliveInterval: 30,
  keepaliveCountMax: 3
})

const connectIconPropsComputed = computed(() =>
  hostType.value ? SIconPropsConst.LinkOff : SIconPropsConst.LinkPlus
)
// 建立会话连接的主机的类型
const hostType = ref<'local' | 'remote' | false>(false)
// 恢复初始化状态
const restoreInit = () => {
  hostType.value = false
  cmdPrompt.value = '>'
  cmdDone.value = false
}
// 建立终端会话连接
const isConnecting = ref(false)
const connect = async () => {
  cmdDone.value = false
  cmdExecDataPre.value = ''
  cmdDoneCnt = 0

  // 如果当前已经与本机终端建立会话，则将其关闭
  if (hostType.value === 'local') {
    const res = await api.shellSessionClose(name)
    terminalApiPushMsg.byResult(name, res)
    if (!res.status) return
  }
  // 如果当前已经与远程主机建立会话，则将其关闭
  else if (hostType.value === 'remote') {
    const res = await api.SSHSessionClose(name)
    terminalApiPushMsg.byResult(name, res)
    if (!res.status) return
  }
  await ElLoadingGlobal({
    isLoading: () => isConnecting.value,
    options: { text: '建立会话连接中，请稍候...' },
    handler: async () => {
      isConnecting.value = true
      // 标记目前未与任何主机建立会话连接
      hostType.value = false
      // 命令未执行完成
      cmdDone.value = false
      // 与远程主机建立会话连接
      if (
        hostConnConfSelected.value &&
        hostConnConfSelected.value.host &&
        hostConnConfSelected.value.host !== 'localhost' &&
        hostConnConfSelected.value.host !== '127.0.0.1'
      ) {
        const res = await api.SSHSessionOpen(name, {
          ...hostConnConfSelected.value,
          // 由于超时配置项不起作用，所以目前暂时取消连接超时重试相关内容；
          reconnect: false,
          reconnectTries: 0,
          reconnectDelay: 0,
          keepaliveInterval: settingsForm.value.keepaliveInterval * 1000,
          keepaliveCountMax: settingsForm.value.keepaliveCountMax
        })
        terminalApiPushMsg.byResult(name, res)
        hostType.value = res.status ? 'remote' : false
        await api.SSHSessionExec(name, tagCmdDone())
      }
      // 与本机终端建立会话连接
      else {
        const res = await window.api.shellSessionOpen(name)
        terminalApiPushMsg.byResult(name, res)
        hostType.value = res.status ? 'local' : false
        await api.shellSessionExec(name, tagCmdDone())
      }
      // 建立会话未成功
      if (!hostType.value) restoreInit()
      isConnecting.value = false
    }
  })
  return hostType.value
}
// 断开终端会话连接
const disconnect = async () => {
  let res: ResultType | null = null
  // 如果存在会话连接，则将其关闭
  if (hostType.value === 'local') {
    res = await api.shellSessionClose(name)
  } else if (hostType.value === 'remote') {
    res = await api.SSHSessionClose(name)
  } else {
    terminalApiPushMsg.info(name, '还未与本机或远程主机建立会话连接，无需断开连接')
  }
  restoreInit()
  if (!res) return
  terminalApiPushMsg.byResult(name, res)
}

// 执行命令
const execCmd = async (command: string, isTagCmdDone: boolean = true) => {
  // 命令未执行完成
  cmdDone.value = false
  cmdExecDataPre.value = ''
  cmdDoneCnt = 0
  let res: ResultType | null = null
  if (hostType.value === 'remote') {
    res = await api.SSHSessionExec(name, command)
    // 打上命令执行完成标记
    if (isTagCmdDone) await api.SSHSessionExec(name, tagCmdDone())
  } else if (hostType.value === 'local') {
    res = await api.shellSessionExec(name, command)
    // 打上命令执行完成标记
    if (isTagCmdDone) await api.shellSessionExec(name, tagCmdDone())
  }
  return res
}
const execCmdHandler = async (
  key: string,
  command: string,
  success: SuccessFunc,
  failed: FailedFunc
) => {
  if (!hostType.value) {
    terminalApiPushMsg.warning(name, `还未与本机或远程主机建立会话连接，无法执行命令（${command}）`)
    success()
    return
  }
  let res = await execCmd(command)
  if (!res) return
  if (res.status) success()
  else {
    failed(res.message + '\n' + (res.data || ''))
  }
}

// 清除终端输出
const clearLogIconProps = SIconPropsConst.TrashX
const clearLog = () => TerminalApi.clearLog(name)

// 组件卸载前的处理
onBeforeUnmount(async () => {
  await disconnect()
  // 移除所有进程通信监听器
  removeAllIpcListeners(name)
})

defineExpose({
  connect,
  execCmd,
  isConnected: () => hostType.value
})

const isOptionsCollapse = ref(false)
</script>

<template>
  <div class="box-border w-full h-full flex flex-col items-stretch">
    <el-scrollbar class="!h-[fit-content]">
      <div class="box-border px-2 flex justify-between items-center" :class="[show ? 'pb-2' : '']">
        <div v-show="show" class="pr-2 flex items-center gap-2">
          <host-selector :select="selectHostConnConf" />
          <x-collapse-btn
            collapse
            :icon-props="settingIconProps"
            text="服务器连接配置"
            :click="() => (isShowSettings = true)"
          />
          <x-collapse-btn
            collapse
            :icon-props="clearLogIconProps"
            text="清除终端输出"
            :click="clearLog"
          />
          <x-collapse-btn
            collapse
            :icon-props="connectIconPropsComputed"
            :text="hostType ? '断开连接' : '建立连接'"
            :btn-props="{ type: hostType ? 'danger' : 'primary' }"
            :click="() => (hostType ? disconnect() : connect())"
          />
        </div>
        <s-contact
          :contact="[() => (isOptionsCollapse = true)]"
          :restore="[() => (isOptionsCollapse = false)]"
        >
          <div class="flex items-center gap-2">
            <slot name="options-suf" :collapse="isOptionsCollapse" />
          </div>
        </s-contact>
      </div>
    </el-scrollbar>
    <div v-show="show" class="flex-1">
      <terminal
        :name="name"
        theme="dark"
        :show-header="false"
        :context="cmdPrompt"
        context-suffix=""
        :enable-default-command="false"
        @exec-cmd="execCmdHandler"
      />
    </div>
  </div>
  <el-dialog v-model="isShowSettings" class="max-w-84" width="80%" title="服务器连接配置">
    <div class="mb-4">
      <el-alert
        title="该配置在与本机终端建立会话时不生效"
        type="info"
        show-icon
        :closable="false"
        effect="dark"
      />
    </div>
    <el-form label-position="top">
      <el-form-item label="检测是否与服务器保持连接的时间间隔">
        <el-input-number v-model="settingsForm.keepaliveInterval" :min="0" :max="60" :step="1">
          <template #suffix>s</template>
        </el-input-number>
      </el-form-item>
      <el-form-item label="所允许的检测连接未应答的最大次数">
        <el-input-number v-model="settingsForm.keepaliveCountMax" :min="1" :max="10" :step="1" />
      </el-form-item>
    </el-form>
  </el-dialog>
</template>

<style scoped></style>
